home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
AM/FM: Amiga Musicians' Freeware Magazine 1
/
AM-FM 1.adf
/
text
/
TechCorner.pp
/
TechCorner
Wrap
Text File
|
1991-10-03
|
25KB
|
609 lines
MIDI and the serial port
~~~~~~~~~~~~~~~~~~~~~~~~
by Teijo Kinnunen (02-Jul-1991)
Welcome to the first TechCorner column of AM/FM!! This column is
directed to programmers, and to anybody who is interested to know how
the tricks/things of the music programs actually work.
What I'm going to give you in this and the future issues of AM/FM
is tutorials about programming sound and music on the Amiga, with
example programs and routines you can use in your own programs.
First (before you get bored...), I would like to say that all feedback
about this column is welcome! I'd like hear from you which topics you
would like me to cover in future issues of AM/FM.
My address is: Teijo Kinnunen
~~~~~~~~~~~~~~ Oksantie 19
SF-86300 OULAINEN
FINLAND
If you want me to reply, please send a self-addressed envelope, and
a small sum of money or international reply coupons to cover the postage.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now let's get started...as the headline says, I'm going to tell how
to make serial routines for MIDI usage (these routines can, of course,
be used for other purposes too).
I will use the MIDI routines I've ripped :^) from MED as a basis of the
example routines, because they're well-tested and quite fast, but simple.
I will explain as clearly as possible how these routines work.
The routines are written in assembly language for maximum speed.
Please feel free to use the routines in your own programs!
Actually there are two approaches of using the serial port. The first
method is using the "serial.device" routines. The second is to use the
hardware directly. I will cover the latter method, because it's somewhat
easier, and because it can be used from an interrupt (which is quite
important in music programs, for smooth'n'steady output). Note that it
IS possible to use the hardware directly in a manner which works well
in a multitasking system, and that's exactly how we're going to do it.
The very first thing we MUST do is to allocate the serial port for our
exclusive use. This is VERY important!! We WON'T use methods of some
programmers who don't know that the Amiga is a multitasking computer.
The serial port is owned by "misc.resource", and if we ask kindly,
it may lend the port to us. Misc.resource contains two routines that we
will use: AllocMiscResource() and FreeMiscResource(). These routines
are used for allocating/freeing some of the miscellaneous hardware
resources (such as the serial port).
(Resources are a kind of "libraries" that handle the sharing of the
low-level hardware resources in the multitasking environment of the
Amiga. For more information, read the Rom Kernel Manuals.)
So, our first routine is:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GetSer: move.l a6,-(sp) ;remember: only a0/a1/d0/d1 may be trashed!
movea.l 4,a6 ;ExecBase
;First we use the exec routine OpenResource() to open the misc.resource.
;I usually enter directly the offsets of the ROM routines, you can use
;the LVO's etc. if you want.
moveq #0,d0 ;any version of the resource is OK
lea miscresname(pc),a1
jsr -$1f2(a6) ;OpenResource()
;Then we test if "misc.resource" were opened. It's in ROM, so this should
;never fail, but we'll check it anyway.
tst.l d0
beq.s GS_error
;The pointer to MiscResBase must be saved...it'll be needed later.
move.l d0,miscresbase
;It must be moved to a6 for the AllocMiscResource() call.
move.l d0,a6
;The first argument of AllocMiscResource(), is the number of the resource
;we want to allocate. In this case it's MR_SERIALPORT, which is defined
;in "resources/misc.i". We don't load the include file, because we know
;it's 0.
moveq #0,d0
;AllocMiscResource() requires the name of the program that tries to
;allocate the resource (never set it to zero!!). If any other program
;tries to allocate the serial port, it will get the name of our program
;(so it can e.g. print an error message "Serial port allocated by MyProggie").
lea myname(pc),a1
;The offset of AllocMiscResource() is -6 (defined in resources/misc.i).
jsr -$6(a6) ;AllocMiscResource()
;If d0 contains zero after the call, the serial port was successfully
;allocated and is now ours. If not, we should exit with error.
tst.l d0
bne.s GS_error
;It's always good to keep track of the resources we've allocated.
;We'll now set a single byte labelled 'serportalloc', so we remember
;that we own that port.
st serportalloc
;Now it's done. We will return 0 in d0, because no errors happened.
moveq #0,d0
GS_exit move.l (sp)+,a6 ;restore a6
rts
;If an error happened, we'll just return -1.
GS_error moveq #-1,d0
bra.s GS_exit
miscresname dc.b 'misc.resource',0
serportalloc dc.b 0
myname dc.b 'MyProggie',0 ;put the name of your program here
even ;longwords must be aligned (ever heard of that?)
miscresbase dc.l 0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now the bad news: This routine may fail even if no program is actually
using the serial port. But don't panic yet...
The reason for that is the "serial.device". If you've been using a program
that does serial IO using the serial.device, the serial port is still
owned by the serial.device even if the program was no longer running. The
serial.device keeps itself the port as long as it's in memory, so we must
try to "flush" the device from the memory. Before removing itself, the
serial.device frees all its resources, including the serial port.
So, here's another routine that tries to allocate the serial port using
the above routine, and if that fails, it will attempt to flush the
serial.device and try again.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;We will precede the function name with '_', so it can be called from
;routines made in C (XDEF will be required then)
_GetSerial move.l a6,-(sp) ;a6 will be trashed
bsr.s GetSer ;Try without flushing serial.device
;If d0 == 0 we succeeded, so there's no need to flush the device.
tst.l d0
beq.s GSerial_exit
;In the case it didn't succeed, we'll have to flush serial.device.
;First it's best to disable multitasking, because we're accessing
;the device list of ExecBase.
movea.l 4,a6
jsr -$84(a6) ;Forbid()
;Then we'll have to find the base structure of serial.device. The devices
;are held in a device list (located at offset $15E of ExecBase). We use
;the routine FindName() to find an entry called serial.device.
lea $15e(a6),a0 ;a0 = pointer of the list
lea serdevname(pc),a1 ;a1 = name ("serial.device")
jsr -$114(a6) ;FindName()
;The pointer is in d0. If it's zero, serial.device was not found.
;In that case we'll skip the RemDevice()-part.
tst.l d0
beq.s GSerial_nosd
;We found the serial.device. Now we just call RemDevice() which does the
;dirty work. If serial.device is currently in use, this does nothing
;(actually, it may set the delayed expunge flag, but it doesn't matter....)
move.l d0,a1
jsr -$1b6(a6) ;RemDevice()
;Enable multitasking again..
GSerial_nosd jsr -$8a(a6) ;Permit()
;Actually, RemDevice returns an error code if it fails, but we just
;try to call the GetSer routine again.
bsr.s GetSer
;The return code from GetSer in d0 is also our return code.
GSerial_exit move.l (sp)+,a6
rts
serdevname dc.b 'serial.device',0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
So, now we have the serial port. Then we have to talk a bit about the
mechanism of the serial output routine. Here's how it works:
We have one routine, called _AddMIDIData, which handles all output and
does some optimization (the running status byte). This routine puts the
data into a circular output buffer (128 bytes). Then there's a serial
interrupt that runs asynchronously, fetches the bytes from the output
buffer and pushes them to SERDAT register. The serial interrupt occurs
automatically when the hardware has handled the most recent byte, and
is ready to accept new data.
First, we must do some initialization: The interrupt must be installed,
and the serial output speed must be set.
Now we calculate the output speed: The serial (as well as the audio) hardware
measures the time as 'periods'. We also know that the MIDI speed is about
31200 bps (bits per second). So, time required to output one bit is 1/31200
seconds (0,000032051 s). Then we look at the page 240 of the Hardware
Reference Manual, and read: "If you consider the contents of these bits to
be the number N, then N+1 color clocks (each 279.4 ns) occur between samples
of the state of the input pin......" So, if each 'period' is 279.4 ns
(0,0000002794 s), N+1 will be 0,000032051s / 0,0000002794 s, which is about
114,7136722, rounded 115. Therefore, N will be 115 - 1 = 114. This actually
gives us baud rate of about 31396 bps, which is accurate enough.
Note that the above calculation is for U.S. machines. The clock speed is
slightly different on NTSC and PAL machines. The period value of 114 works
fine with both. The actual value for PAL would be 113.
Then we must initialize the TBE (Transmit Buffer Empty) interrupt.
This is naturally done using SetIntVector() instead of touching the
zero page autovectors.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
_InitSerial move.l a6,-(sp)
;We check if the serial port was actually allocated. The caller of
;this routine *should* take care of that!
;(note that we use move.b instead of tst.b, because we can then use
; the pc-relative addressing, saving 2 bytes of memory)
move.b serportalloc(pc),d0
beq.s InitSer_exit
;Now, set the baud rate (as calculated above).
move.w #114,$dff032 ;$dff032 = SERPER
;Install the TBE handler
moveq #0,d0 ;INTB_TBE (hardware/intbits.i)
lea tbeinterrupt(pc),a1 ;see definition below
movea.l 4,a6
jsr -$a2(a6) ;SetIntVector()
move.l d0,prevtbe ;store the addr. of former interrupt
;Now we're ready to enable that interrupt. We'll set the TBE
;bit of INTENA ($dff09a). (The interrupt won't occur yet, however.)
move.w #$8001,$dff09a
InitSer_exit move.l (sp)+,a6
rts
prevtbe dc.l 0
;This is an Interrupt structure (see exec/interrupts.i).
;The IS_DATA field is set to point to the output buffer pointer (see below),
;so the address will be loaded into a1 beforehand.
;SerIntHandler is the pointer to the actual interrupt code (see below).
tbeinterrupt dc.w 0,0,0,0,0
dc.l tbename,buffptr,SerIntHandler
;We should give our interrupt a name.
tbename dc.b 'MyProggie serial interrupt',0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now begins the critical part...below is a listing of the serial interrupt
handler routine. There are numerous optimizations to make it faster, for
example, the order of the fields in the data section must not be changed
(because address register relative data addressing is used).
Then I refresh your memory by telling which registers initially set
by the Exec interrupt code we are using:
a0 = pointer to the custom chips ($dff000)
a1 = IS_DATA of the Interrupt structure, which was set to point to
the buffptr-field above (note: a1 = ADDRESS of buffptr, not
the contents of it)
a6 = ExecBase
In addition, registers a5, d0 and d1 may (and will) be trashed.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SerIntHandler
;We disable interrupts, so that a music routine running on higher level
;interrupts can't change anything while we're working.
move.w #$4000,$9a(a0) ;Master bit of INTENA
;Actually, it's probably not absolutely necessary to increment the
;IDNestCnt of ExecBase. This interrupt wouldn't have occurred if the
;interrupts were disabled. But this is a example of "clean" code, so
;I left it here.
addq.b #1,$126(a6) ;increment ExecBase->IDNestCnt
;First we clear the TBE bit in INTREQ register (that actually caused
;this interrupt). Otherwise the interrupt would be immediately
;retriggered.
move.w #1,$9c(a0) ;$dff09c = INTREQ
;Load into d0 the number of bytes that must be sent (we can send only
;one at a time, however).
move.b bytesinbuff(pc),d0
;If it's zero, there's nothing to send in the buffer.
beq.s SerInt_bufempty
;Now the fun begins...First we get the READ pointer of the buffer
;(buffptr is the WRITE pointer). A1 points to buffptr, so readbuffptr
;is at 4(a1).
movea.l 4(a1),a5
;Then we initialize the data word that will be put into SERDAT register.
;There must be one stop bit.
move.w #$100,d1
;Now the actual data byte must be read (it'll be placed at the lowest byte
;of d1), the read pointer will be incremented, too.
move.b (a5)+,d1
;Then, we'll just push the word into SERDAT register, and the hardware
;takes care of the rest.
move.w d1,$30(a0) ;SERDAT = $dff030
;The byte is now sent. Then we must check and update the pointers.
;We compare the read pointer to a1 (address of buffptr). If they're equal,
;the end of the buffer has been reached, and the pointer must be reset.
cmpa.l a1,a5
bne.s SerInt_resrp ;no need to reset
lea sendbuffer(pc),a5 ;start address of the buffer
SerInt_resrp
;d0 contains the number of bytes in the buffer. It must be decremented.
subq.b #1,d0
move.b d0,8(a1) ;save it, 8(a1) = bytesinbuff
;Finally we save the buffer read pointer...
move.l a5,4(a1)
;enable interrupts...
SerInt_exit subq.b #1,$126(a6)
bge.s SerInt_X
move.w #$c000,$9a(a0)
;and exit.
SerInt_X rts
;Buffer is empty, so we set a flag. AddMIDIData() will then set
;TBE flag of INTREQ, so that the interrupt will be invoked again.
SerInt_bufempty st 9(a1)
bra.s SerInt_exit
sendbuffer ds.b 128
buffptr dc.l sendbuffer ;buffer WRITE pointer
readbuffptr dc.l sendbuffer ;buffer READ pointer
bytesinbuff dc.b 0
;Initially the buffer's empty, so we set this flag.
bufferempty dc.b -1
;Storage for the latest status byte (rsb-optimization).
lastcmdbyte dc.b 0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
At this moment you should be ready to face the most complex routine:
_AddMIDIData.
This routine has two arguments:
a0 = pointer to MIDI data
d0 = length of the data
It copies the MIDI data to the output buffer, and does the running status
optimization (that allows you to leave out subsequent status bytes if
they are the same).
Note that this routine can't handle long data transfers, because
the buffer is only 128 bytes long! To handle e.g. long SysEx messages you
could:
1) increase the buffer size (this may also require some changes from
.b to .w etc..)
2) write an interrupt routine of your own, that can handle long,
continuous buffers.
If - for some reason - your program sends out data faster than these
routines can handle, the buffer may get filled and the data starts to
overlap. No crash or guru will occur, but trashed data may be sent out.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;Registers used:
;a0 = source pointer
;a1 = destination pointer (output buffer)
;a2 = pointer to buffptr (for address register relative data addressing)
;a6 = ExecBase
;d0 = number of bytes left
;d1 = data storage
_AddMIDIData
;Do the serportalloc-check...
move.b serportalloc(pc),d1
beq.s AMD_rts
movem.l a2/a6,-(sp) ;these will be trashed
movea.l 4,a6 ;get ExecBase...
;Disable(), so that nobody (the TBE interrupt) can disturb us...
move.w #$4000,$dff09a
addq.b #1,$126(a6)
;Load ptr to buffer pointer into a2.
lea buffptr(pc),a2
;First test if the buffer is empty.
tst.b 9(a2) ;uses the data struct defined above
beq.s AMD_noTBEreq
;We clear the flag, the buffer will not be empty after a while.
clr.b 9(a1)
;It was empty, so we request the TBE to happen (because interrupts
;are disabled, the interrupt won't occur immediately).
move.w #$8001,$dff09c
;Get the buffer write pointer
AMD_noTBEreq movea.l (a2),a1
;Here begins the loop which fetches every byte and pushes it into the
;output buffer.
AMD_dataloop move.b (a0)+,d1 ;get a byte
;We check if it's a status byte (for handling the optimization).
;It is status byte if it's >= $80.
bpl.s AMD_nostatus
;It probably was a status byte. It may, however, be a realtime
;message (>= $F0), so we do another check.
cmp.b #$ef,d1
bhi.s AMD_nostatus
;Now we are sure that it's a status byte. Just check if it's the
;same as the previous status byte that was sent.
cmp.b lastcmdbyte(pc),d1
;If it was the same, we skip the part that inserts the byte into
;the output buffer.
beq.s AMD_noinsbyte
;In the case it wasn't, we save the status byte.
move.b d1,10(a2) ;10(a2) = lastcmdbyte
;Then we push the byte into the buffer...
AMD_nostatus move.b d1,(a1)+
;Add the byte counter
addq.b #1,8(a2) ;8(a2) = bytesinbuff
AMD_noinsbyte cmpa.l a2,a1 ;end of buffer reached?
bne.s AMD_noptrreset
;Reset the buffer write pointer...
lea sendbuffer(pc),a1
;Finally, check if there are any bytes still left, and if yes,
;jump to the start of the loop.
AMD_noptrreset subq.b #1,d0
bne.s AMD_dataloop
;Save the write buffer pointer.
move.l a1,(a2)
;Enable interrupts, and exit.
subq.b #1,$126(a6)
bge.s AMD_x
move.w #$c000,$dff09a
AMD_x movem.l (sp)+,a2/a6
AMD_rts rts
;data structure defined above
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
So, now we have routines for putting out MIDI data. But what about INPUT??
The input is definitely more trickier than output, as the routine should be
able to interpret MIDI data, ignore things it's not interested in, handle
realtime messages, SysEx, etc.etc... Therefore, it's impossible to make an
all-purpose input routine.
However, as an example, I'll present you a simplified version of the input
handler of MED. This routine recognizes only Note On and Note Off messages
and ignores everything else. It has a three-byte buffer where it places the
incoming notes. The main task is signalled when a note is read.
The running status byte is recognized.
The interrupt to use is the RBF (Receive Buffer Full) interrupt, which runs
as a level 6 (= high level) interrupt. However, this presents us another
problem: If the play routine uses a level 6 interrupt (which is the case
when CIAB timers are used), bytes may be lost because of the time needed
to handle the interrupt (If the RBF occurs after starting executing the
play routine, the interrupt will be handled AFTER exiting the play routine,
because interrupts running on the same priority won't overlap. If another
byte comes in BEFORE handling that interrupt, the previous byte will be
lost). In MED (V3.11) I've get round that by using a copper interrupt which
is called by the CIAB interrupt.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;TO BE ADDED TO _InitSerial
...
moveq #11,d0 ;RBF
lea rbfinterrupt(pc),a1
jsr -$a2(a6) ;SetIntVector()
move.l d0,prevrbf
...
;IN ADDITION: move.w #$8001,$dff09a SHOULD BE REPLACED WITH
move.w #$8801,$dff09a
;to activate both TBE and RBF
prevrbf dc.l 0
rbfinterrupt dc.w 0,0,0,0,0
dc.l rbfname,recmidi,RBFIntHandler
;Three external references are required (the main program should
;define these)
xref _maintsk ;the task to be signalled
xref _sigmask ;the signal mask
xref _inputbuff ;a 3-byte input note buffer
RBFIntHandler
;Immediately read the byte from SERDATR-register.
move.w $18(a0),d0 ;SERDATR
;Then clear the RBF-bit of INTREQ
move.w #$0800,$9c(a0)
tst.b d0
bpl.s RBF_nostatus
;If it's greater than $F7, it's a single-byte realtime message.
;We just ignore it.
cmp.b #$f7,d0
bhi.s RBF_exit
;The status (command) byte is saved to the first of the 3 bytes.
move.b d0,(a1)
;The read counter is then reset.
clr.b 3(a1)
RBF_exit rts
RBF_nostatus moveq #0,d1
;Get the read counter..
move.b 3(a1),d1
;Save the byte..
move.b d0,1(a1,d1.w)
;Increment counter...
addq.b #1,d1
;If it's 2 or greater, we signal the main task
cmp.b #2,d1
bge.s RBF_signal
;If not, we just save the counter, and exit.
move.b d1,3(a1)
rts
;First we reset the read counter so that the next byte will be
;written into the second byte (for handling the running status)
RBF_signal clr.b 3(a1)
;Now get the status byte (command).
move.b (a1),d0
;Mask out the channel.
and.b #$f0,d0
;Test if it's Note On (this could be easily extended to handle
;other messages e.g. controller, pitchbender...)
cmp.b #$90,d0
beq.s RBF_sig
;Try if it's Note Off.
cmp.b #$80,d0
;If it isn't we just exit.
bne.s RBF_nosig
;Copy the note data to the input note buffer of the task.
RBF_sig lea _inputbuff,a0
move.b (a1)+,(a0)+
move.b (a1)+,(a0)+
move.b (a1),(a0)
;And send it a signal.
movea.l _maintsk,a1
move.l _sigmask,d0
jsr -$144(a6) ;Signal()
RBF_nosig rts
;A temporary three-byte buffer. The fourth byte is the number of the
;byte most recently written.
recmidi: dc.b 0,0,0,0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These were the main MIDI routines. We still need a routine to free the
allocated resources.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
_FreeSerial move.l a6,-(sp)
;First let's make a check if the serial port is allocated. This way the
;application programmer can call this routine on exit, even if GetSerial
;had failed.
tst.b serportalloc
beq.s FreeSer_xit
;First we disable the serial interrupts, so that they won't occur
;after resetting the interrupts.
move.w #$0801,$dff09a ;disables both RBF and TBE
movea.l 4,a6
;Now resetting the TBE interrupt.
moveq #0,d0
move.l prevtbe(pc),a1
jsr -$a2(a6) ;SetIntVector()
***** This part should be ignored if no RBF interrupt is installed *********
moveq #11,d0
move.l prevrbf(pc),a1
jsr -$a2(a6) ;SetIntVector()
****************************************************************************
;Then free the serial port using routine FreeMiscResource() (offset -$C)
movea.l miscresbase(pc),a6
moveq #0,d0
jsr -$c(a6) ;FreeMiscResource()
clr.b serportalloc
FreeSer_xit move.l (sp)+,a6
rts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Still there's one routine that may be useful. There may be need to reset
the above routines by flushing the buffers and forgetting the previous
status byte. Here's it:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
_ResetMIDI: move.l a6,-(sp)
;We keep interrupts disabled, the interrupts might otherwise touch
;the data we're resetting.
movea.l 4,a6
move.w #$4000,$dff09a
addq.b #1,$126(a6)
;a1 = pointer to buffptr, for address register relative addressing
lea buffptr(pc),a1
lea sendbuffer(pc),a0
;reset both the buffer read and write pointers
move.l a0,(a1)+
move.l a0,(a1)+
;clear the 'bytesinbuff' counter
clr.b (a1)+
;skip the 'bufferempty' (it is reset by the TBE interrupt when it's
;finished its job)
addq.l #1,a1
;clear the 'lastcmdbyte', to reset the running status byte system
clr.b (a1)
subq.b #1,$126(a6)
bge.s ResetM_x
move.w #$c000,$dff09a
ResetM_x move.l (sp)+,a6
rts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I've collected all these routines into one file: midiroutines.a
You can insert the routines directly into your programs (remove XDEFs
and XREFs after doing that) or if your program consists of several object
files, you can link the object code using Blink.
There's a single flag, INPUT, which is used to include/exclude the portions
of code that handle MIDI input. Set it to 1 if you want to have the input
routines.
For those who want to use the routines in C-programs (SAS/Lattice C V5),
I've provided 'midiroutines.h', a header file that defines the prototypes
for the functions.
I've also provided the object files (so that C-programmers don't require
to use an assembler):
midiroutines.o INPUT set to 0
midiroutines_i.o INPUT set to 1
Also, note that _InitSerial call is not required during startup (in fact,
that function even hasn't been XDEF'd). The 'midiroutines.a' automatically
performs it during serial port allocation.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Finally, I've written two programs to demonstrate how to use these routines.
The first one is called 'example1', a C-program which, respectively,
plays the C-major chord. It demonstrates only how to output data.
The second example is more interesting. It demonstrates both the output and
input procedures. When you run it, it waits until you press a key on your
MIDI keyboard (supposing you've connected it to the MIDI IN of the Amiga).
Then it displays the name of the note you pressed, and sends it out
immediately, transposed 12 halfsteps (one octave) up. Exit by pressing
CTRL-C. This program is called 'transposedemo' and it's written in
assembler.
Both the executables, sources, and the .lnk files are included. Have fun
with them!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~